package com.zeng.sboot.redis.demo.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;

import java.lang.reflect.Method;

/**
 * 每一种方式显示 配置 Redis
 *
 * 如果需要自定义 key 的话
 * 继承 org.springframework.cache.annotation.CachingConfigurerSupport 并重写方法keyGenerator()
 */

@Configuration //此类是一个配置类
@EnableCaching //开启 缓存
public class RedisInitialize extends CachingConfigurerSupport {

    private static Logger logger = LoggerFactory.getLogger(RedisInitialize.class);


    /**
     * 自定义创建 RedisConnectionFactory 对象
     *
     * 创建方法有两种：
     * 1）直接在 application.properties 配置文件中加入 redis 的配置, spring 会自动创建 RedisConnectionFactory 对象
     *    然后在spring容器中可以直接注入使用 RedisConnectionFactory 对象
     * 2）可以如下配置显示配置 RedisConnectionFactory 对象, 创建好之后可以在容器中注入并使用之
     *
     * spring 已准备 RedisTemplate<String, Object> 和 StringRedisTemplate<String, String> 两个对象, 在容器中也可以直接注入使用
     *
     * @return
     */
    @Bean // 如果不配置 默认是 localhost:6379   RedisStandaloneConfiguration 这个对象
    public RedisConnectionFactory redisConnectionFactory(){//自定义创建的对象 RedisConnectionFactory
        // 1. 创建 JedisPoolConfig
        // public JedisConnectionFactory(JedisPoolConfig poolConfig)
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(100);//最大连接数
        poolConfig.setMaxIdle(30);//最大空闲数
        poolConfig.setMinIdle(10);//最小空闲数
        poolConfig.setNumTestsPerEvictionRun(1024);//每次释放连接的最大数目
        poolConfig.setTimeBetweenEvictionRunsMillis(30000);//释放连接的扫描间隔（毫秒）
        poolConfig.setMinEvictableIdleTimeMillis(1800000);//连接最小空闲时间
        poolConfig.setSoftMinEvictableIdleTimeMillis(10000);//连接空闲多久后释放, 当空闲时间>该值 且 空闲连接>最大空闲连接数 时直接释放
        poolConfig.setMaxWaitMillis(1500);//获取连接时的最大等待毫秒数,小于零:阻塞不确定的时间,默认-1
        poolConfig.setTestOnBorrow(true);//在获取连接的时候检查有效性, 默认false
        poolConfig.setTestWhileIdle(true);//在空闲时检查有效性, 默认false
        poolConfig.setBlockWhenExhausted(false);//连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true

        JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(poolConfig);
        jedisConnectionFactory.setPort(6379);
        jedisConnectionFactory.setHostName("192.168.1.1");
        jedisConnectionFactory.setDatabase(0);
        //jedisConnectionFactory.setPassword();
        return jedisConnectionFactory;
    }


    /**
     * 自定义创建 RedisTemplate<String, Object> 对象
     *
     * spring 准备 RedisTemplate<String, Object> 和 StringRedisTemplate<String, String> 两个对象, 可以直接注入使用
     * 也可以通过下述代码创建这两个对象, 这样更加灵活的可以添加 如 序列化器 配置
     *
     * 当保存数据时,key和value都会被系列化成JSON数据,取出来时会被反系列化成对象,key和value都会使用用系列化器
     * spring data redis 提供了多个系列化器
     * -）GenericToStringSerializer：使用Spring转换服务进行序列化;
     * -）JdkSerializationRedisSerializer：使用Jackson 1, 将对象序列化为JSON;
     * -）Jackson2JsonRedisSerializer：使用Jackson 2, 将对象序列化为JSON;
     * -）OxmSerializer：使用Spring O/X映射的编排器和解排器（marshaler和unmarshaler）实现序列化,用于XML序列化;
     * -）StringRedisSerializer：序列化String类型的key和value
     *
     * RedisTemplate 会默认使用 JdkSerializationRedisSerializer, 这意味着key和value都会通过Java进行序列化.
     * StringRedisTemplate 默认会使用 StringRedisSerializer
     *
     * 需求）
     * 假设当使用RedisTemplate的时候, 我们希望将 Product 类型的 value 序列化为 JSON，而 key 是 String 类型.
     * RedisTemplate 的 setKeySerializer() 和 setValueSerializer() 方法就需要如下述代码所示
     *
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){//注入RedisConnectionFactory:注入的是自定义创建的对象
        // 创建一个模板类
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        // 将刚才的redis连接工厂设置到模板类中
        template.setConnectionFactory(redisConnectionFactory);
        // 设置key的序列化器
        template.setKeySerializer(new StringRedisSerializer());
        // 配置value的序列化器
        // 使用Jackson 2, 将对象序列化为JSON
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        // json转对象类, 不设置默认的会将json转成hashmap
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 设置value的序列化器
        template.setValueSerializer(jackson2JsonRedisSerializer);

        return template;
    }


    /**
     * 自定义创建 StringRedisTemplate<String, String> 对象
     *
     * @return
     */
    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory){//注入RedisConnectionFactory:注入的是自定义创建的对象
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
        return stringRedisTemplate;
    }


    /**
     * 声明缓存管理器, 会创建一个切面（aspect）并触发Spring缓存注解的切点（pointcut）
     * 根据类或者方法所使用的注解以及缓存的状态, 这个切面会从缓存中获取数据, 将数据添加到缓存之中或者从缓存中移除某个值
     * 在类方法上使用 @Cacheable("prudCache")
     *
     * 1）可以在 application.properties 中配置,spring会自动创建好 RedisConnectionFactory 对象,
     *    并注入进到方法参数中使用
     * 2）也可以使用本类创建的 RedisConnectionFactory 对象, 使用 @Autowired并注入进来使用
     *
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory){
//        RedisCacheManager redisCacheManager =
//                RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory).build();
//        RedisCacheManager redisCacheManager = RedisCacheManager.create(redisConnectionFactory);
        RedisCacheManager redisCacheManager = new RedisCacheManager(stringRedisTemplate(redisConnectionFactory));
        return redisCacheManager;
    }


    /**
     * 当使用 @Cacheable 时会自动创建一个 类名.方法名 的键
     * 缓存中的 key 此处指定为 类名.方法名
     * 调用Redis缓存Key: com.zeng.sboot.redis.demo.controller.JedisController.string
     * @return
     */
    @Override // 快捷键 Ctrl + O
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            public Object generate(Object obj, Method method, Object... objs) {
                //格式化缓存key字符串
                StringBuilder sb = new StringBuilder();
                //追加类名
                sb.append(obj.getClass().getName());
                //追加方法名
                sb.append(".");
                sb.append(method.getName());
                //遍历参数并且追加
                for(Object o : objs){
                    sb.append(o.toString());
                }
                System.out.println("调用Redis缓存Key: " + sb.toString());
                logger.info("调用Redis缓存Key为 {}",sb.toString());
                return sb.toString();
            }
        };
    }
}